"""
CircularLinkedList is a data structure in which each node has a reference to the next node, forming a circular chain. The last node in the list points back to the first node, creating a circular structure.

Properties:
- Each node in a CircularLinkedList contains two parts: data and a reference to the next node.
- The last node in the list points back to the first node, forming a circular structure.
- CircularLinkedList allows for efficient traversal of the entire list, as it doesn't have a distinct end point.
- The list can be traversed starting from any node, as the circular structure ensures that all nodes are reachable.

Operations:

1. Initialization:
   - `CircularLinkedList()`: Creates an empty circular linked list.

2. Basic Operations:
   - `add(data)`: Adds an element to the circular linked list.
   - `insert(data, position)`: Inserts an element at the specified position in the circular linked list.
   - `remove(data)`: Removes the first occurrence of the specified element from the circular linked list.
   - `remove_at(position)`: Removes the element at the specified position from the circular linked list.
   - `search(data)`: Searches for the first occurrence of the specified element in the circular linked list and returns its position.
   - `get(position)`: Returns the element at the specified position in the circular linked list.
   - `size()`: Returns the number of elements in the circular linked list.
   - `is_empty()`: Checks if the circular linked list is empty.

Example Usage:
```
circular_linked_list = CircularLinkedList()

circular_linked_list.add(5)
circular_linked_list.insert(7, 1)
circular_linked_list.remove(5)
print(circular_linked_list.search(7))  # Output: 0
print(circular_linked_list.get(0))  # Output: 7
print(circular_linked_list.size())  # Output: 1
print(circular_linked_list.is_empty())  # Output: False
```

Time and Space Complexity:
- Adding or removing elements at the end of a circular linked list has a time complexity of O(1).
- Inserting or removing elements at a specific position in the circular linked list has a time complexity of O(n), where n is the number of elements.
- Searching for an element in the circular linked list has a time complexity of O(n), as it may require traversing the entire list.
- The space complexity of a circular linked list is O(n), where n is the number of elements, as each element requires a node with a reference to the next node.
"""

class Node:
    def __init__(self, value=None):
        """
        Initialize a node for use in a circular linked list.

        Args:
            value: The value to be stored in the node.
            next: The reference to the next node in the list. Defaults to None.
        """
        self.value = value
        self.next = None


class CircularLinkedList:
    def __init__(self):
        """
        Initialize an empty circular linked list.

        Attributes:
            head (Node): The first node in the list.
            length (int): The number of elements in the list.
        """
        self.head = None
        self.length = 0

    def append(self, value):
        """
        Insert a new element at the end of the list.

        Args:
            value: The value to be stored in the new node.
        """
        new_node = Node(value=value)
        if self.head is None:
            self.head = new_node
            new_node.next = self.head
        else:
            current = self.head
            while current.next != self.head:
                current = current.next
            current.next = new_node
            new_node.next = self.head
        self.length += 1

    def prepend(self, value):
        """
        Insert a new element at the beginning of the list.

        Args:
            value: The value to be stored in the new node.
        """
        new_node = Node(value=value)
        if self.head is None:
            self.head = new_node
            new_node.next = self.head
        else:
            current = self.head
            while current.next != self.head:
                current = current.next
            current.next = new_node
            new_node.next = self.head
            self.head = new_node
        self.length += 1

    def find_by_index(self, index):
        """
        Find and return a node at a particular index.

        Args:
            index (int): The index of the node.

        Returns:
            The node at the specified index. If the index is out of range, return None.
        """
        if index < 0 or index >= self.length:
            return None
        current = self.head
        for _ in range(index):
            current = current.next
        return current

    def find_by_value(self, value):
        """
        Find and return a node with a particular value.

        Args:
            value: The value to search for.

        Returns:
            The first node that contains the value. If no such node exists, return None.
        """
        current = self.head
        for _ in range(self.length):
            if current.value == value:
                return current
            current = current.next
            if current == self.head:
                break
        return None

    def find_min_node(self):
        """
        Find and return the node with the minimum value.

        Returns:
            The node with the smallest value. If the list is empty, return None.
        """
        if self.head is None:
            return None
        min_node = self.head
        current = self.head.next
        while current != self.head:
            if current.value < min_node.value:
                min_node = current
            current = current.next
        return min_node

    def delete_by_index(self, index):
        """
        Delete the node at the specified index.

        Args:
            index (int): The index of the node to delete.

        Raises:
            IndexError: If the index is out of range.
        """
        if index < 0 or index >= self.length:
            raise IndexError('CircularLinkedList index out of range')
        if self.length == 1:
            self.head = None
        else:
            prev_node = self.find_by_index(index - 1)
            node_to_delete = prev_node.next
            prev_node.next = node_to_delete.next
            if node_to_delete == self.head:
                self.head = prev_node.next
        self.length -= 1

    def delete_by_value(self, value):
        """
        Delete the node with a particular value.

        Args:
            value: The value to search for and delete.

        Raises:
            ValueError: If the value is not found in the linked list.
        """
        if self.head is None:
            raise ValueError('Value not found in linked list')
        if self.head.value == value:
            if self.length == 1:
                self.head = None
            else:
                current = self.head
                while current.next != self.head:
                    current = current.next
                current.next = self.head.next
                self.head = current.next
        else:
            current = self.head
            while current.next != self.head:
                if current.next.value == value:
                    current.next = current.next.next
                    self.length -= 1
                    return
                current = current.next
            raise ValueError('Value not found in linked list')

    def __len__(self):
        """
        Return the number of elements in the circular linked list.

        Returns:
            int: The number of elements in the circular linked list.
        """
        return self.length

    def __repr__(self):
        """
        Return a machine-readable string representation of the CircularLinkedList.

        Returns:
            str: A string representation of the CircularLinkedList in the format '<CircularLinkedList: head_value, length>'.
        """
        if self.head:
            head_value = self.head.value
        else:
            head_value = None

        return f'<CircularLinkedList: head={head_value}, length={self.length}>'

    def __str__(self):
        """
        Convert the circular linked list to a string representation.

        Returns:
            str: A string representation of the circular linked list in the format 'CircularLinkedList: [value1 <-> value2 <-> ... <-> valueN -> head_value]'.
        """
        values = []
        current = self.head
        if current is not None:
            for _ in range(self.length):
                values.append(str(current.value))
                current = current.next
        return 'CircularLinkedList: [' + ' <-> '.join(values) + f' -> {self.head.value}]'


if __name__ == "__main__":
    linked_list = CircularLinkedList()

    linked_list.append(5)
    linked_list.append(10)
    linked_list.append(15)
    linked_list.prepend(1)

    print(linked_list)  # CircularLinkedList: [1 <-> 5 <-> 10 <-> 15 -> 1]

    node = linked_list.find_by_index(2)
    if node:
        print(f"Value at index 2: {node.value}")  # Value at index 2: 10
    else:
        print("Index out of range.")

    node = linked_list.find_by_value(10)
    if node:
        print("Found node with value 10")  # Found node with value 10
    else:
        print("Value not found in linked list.")

    min_node = linked_list.find_min_node()
    if min_node:
        print(f"Node with minimum value: {min_node.value}")  # Node with minimum value: 1
    else:
        print("CircularLinkedList is empty.")

    linked_list.delete_by_index(1)
    print(f"CircularLinkedList after deleting value at index 1: {linked_list}")  # CircularLinkedList after deleting value at index 1: [1 <-> 10 <-> 15 -> 1]

    linked_list.delete_by_value(15)
    print(f"CircularLinkedList after deleting value 15: {linked_list}")  # CircularLinkedList after deleting value 15: [1 <-> 10 -> 1]

    print(repr(linked_list))  # <CircularLinkedList: head=1, length=2>
